/*
 * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2006.
 *
 * Licensed under the Aduna BSD-style license.
 */
 
package edu.tju.commons.net;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;

import edu.tju.commons.net.http.HttpClientUtil;
import edu.tju.commons.text.StringUtil;

/**
 * Utility methods related to URLs, network connections, etc.
 */
public class UrlUtil {

	/**
	 * Normalizes a URL. The following steps are taken to normalize a URL:
	 * <ul>
	 * <li>The protocol is made lower-case.
	 * <li>The host is made lower-case.
	 * <li>A specified port is removed if it matches the default port.
	 * <li>Any query parameters are sorted alphabetically.
	 * <li>Any anchor information is removed.
	 * </ul>
	 */
	public static URL normalizeURL(URL url) {
		// Get the separate fields of the URL
		String protocol = url.getProtocol();
		String host = url.getHost();
		int port = url.getPort();
		String path = url.getPath();
		String query = url.getQuery();

		// Normalize the fields
		protocol = protocol.toLowerCase();
		host = host.toLowerCase();

		if (port == url.getDefaultPort()) {
			port = -1;
		}

		String file = _normalizePath(path);

		if (query != null) {
			query = _normalizeQuery(query);
			file += "?" + query;
		}

		// Create normalized URL
		try {
			return new URL(protocol, host, port, file);
		}
		catch (MalformedURLException e) {
			// Something wrong with the algorithm?
			throw new RuntimeException(e);
		}
	}

	/**
	 * Remove relative references and "mistakes" like double slashes from the
	 * path.
	 *
	 * @param path
	 *        The path to normalize.
	 * @return The normalized path.
	 */
	private static String _normalizePath(String path) {
		String result = path;

		// replace all double slashes with a single slash
		result = StringUtil.gsub("//", "/", result);

		// replace all references to the current directory with nothing
		result = StringUtil.gsub("/./", "/", result);

		// replace all references to the parent directory with nothing
		result = result.replaceAll("/[^/]+/\\.\\./", "/");

		return result;
	}

	/**
	 * Normalizes a query string by sorting the query parameters alpabetically.
	 *
	 * @param query
	 *        The query string to normalize.
	 * @return The normalized query string.
	 */
	private static String _normalizeQuery(String query) {
		SortedSet<String> sortedSet = new TreeSet<String>();

		// Extract key-value pairs from the query string
		StringTokenizer tokenizer = new StringTokenizer(query, "&");
		while (tokenizer.hasMoreTokens()) {
			sortedSet.add(tokenizer.nextToken());
		}

		// Reconstruct query string
		StringBuilder result = new StringBuilder(query.length());
		Iterator<String> iter = sortedSet.iterator();
		while (iter.hasNext()) {
			result.append(iter.next());

			if (iter.hasNext()) {
				result.append("&");
			}
		}

		return result.toString();
	}

	/**
	 * Checks whether two URLs are equal. This is an alternative implementation
	 * for <tt>URL.equals(Object)</tt> which compares hosts by resolving their
	 * IP-addresses, which doesn't work when virtual hosts are involved. Also,
	 * this method does not take into account that the order of query parameters
	 * is irrelevant. This method fixes these issues by checking that:
	 * <ul>
	 * <li>The protocols, if any, are equal (case-insensitive).
	 * <li>The hosts, if any, are equal (case-insensitive).
	 * <li>The port numbers are equal, using the default port number for
	 * comparison when not specified.
	 * <li>The paths are equal (case-sensitive).
	 * <li>The URLs have identical query parameters, ignoring their order, e.g.
	 * <tt>?arg1=abc&amp;arg2=def</tt> is equal to
	 * <tt>?arg2=def&amp;arg1=abc</tt>.
	 * <li>The URLs references, if any, are equal.
	 * </ul>
	 */
	public static boolean urlsEqual(URL url1, URL url2) {
		String protocol1 = url1.getProtocol();
		String host1 = url1.getHost();
		int port1 = url1.getPort();
		String path1 = url1.getPath();
		String query1 = url1.getQuery();
		String ref1 = url1.getRef();

		String protocol2 = url2.getProtocol();
		String host2 = url2.getHost();
		int port2 = url2.getPort();
		String path2 = url2.getPath();
		String query2 = url2.getQuery();
		String ref2 = url2.getRef();

		// Compare protocol, host, port, path and ref
		boolean result = path1.equals(path2)
				&& (host1 == null && host2 == null || host1 != null && host1.equalsIgnoreCase(host2))
				&& (ref1 == null && ref2 == null || ref1 != null && ref1.equals(ref2))
				&& (port1 == port2 || port1 == -1 && port2 == url1.getDefaultPort() || port2 == -1
						&& port1 == url2.getDefaultPort())
				&& (protocol1 == null && protocol2 == null || protocol1 != null && protocol1.equalsIgnoreCase(protocol2));

		if (result == true) {
			// Compare query strings
			result = query1 == null && query2 == null || query1 != null && query1.equals(query2);

			if (result == false && query1 != null && query2 != null) {
				// Query strings not equal, try sorting the parameters
				query1 = _normalizeQuery(query1);
				query2 = _normalizeQuery(query2);

				result = query1.equals(query2);
			}
		}

		return result;
	}

	/**
	 * Builds a query string from the provided key-value-pairs. All spaces are
	 * substituted by '+' characters, and all non US-ASCII characters are
	 * escaped to hexadecimal notation (%xx).
	 */
	public static String buildQueryString(Map<String, String> keyValuePairs) {
		StringBuilder result = new StringBuilder(200);

		Set<Map.Entry<String, String>> entrySet = keyValuePairs.entrySet();
		Iterator<Map.Entry<String, String>> iter = entrySet.iterator();

		while (iter.hasNext()) {
			// Iterate over all key-value pairs
			Map.Entry<String, String> keyValuePair = iter.next();

			String key = keyValuePair.getKey();
			String value = keyValuePair.getValue();

			// Escape both key and value and combine them with an '='
			HttpClientUtil.formUrlEncode(key, result);
			result.append('=');
			HttpClientUtil.formUrlEncode(value, result);

			// If there are more key-value pairs, append an '&'
			if (iter.hasNext()) {
				result.append('&');
			}
		}

		return result.toString();
	}

	/**
	 * Trims a URL to the directory it refers. This means that everything after
	 * the refered directory is removed from the URL. For example, the URL
	 * <tt>http://foo.com/a/b/c/index.jsp?a=b#c</tt> will be trimmed to
	 * <tt>http://foo.com/a/b/c/</tt>, and the URL
	 * <tt>http://foo.com/a/b</tt> will be trimmed to
	 * <tt>http://foo.com/a/</tt>.
	 *
	 * @exception MalformedURLException
	 *            If trimming the URL resulted in a malformed URL.
	 */
	public static URL trimToDirectory(URL url) {
		String path = url.getPath();

		int lastPathSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
		if (lastPathSep != -1) {
			path = path.substring(0, lastPathSep + 1);
		}

		try {
			return new URL(url, path);
		}
		catch (MalformedURLException e) {
			// this should never happen
			throw new RuntimeException(e);
		}
	}
}
